1use super::reach_set2::{ReachSet2Options, reach_set2_backward};
20use crate::copp::copp2::formulation::Topp2Problem;
21use crate::copp::{ApproxOrdering, approx_order};
22use crate::diag::{
23 CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
24 format_duration_human,
25};
26use crate::math::numerical::{LpToleranceOptions, lp_1d};
27use core::f64;
28use itertools::izip;
29
30pub fn topp2_ra(problem: &Topp2Problem, options: &ReachSet2Options) -> Result<Vec<f64>, CoppError> {
49 match options.verbosity {
50 Verbosity::Silent => topp2_ra_core(problem, (options, SilentVerboser)),
51 Verbosity::Summary => topp2_ra_core(problem, (options, SummaryVerboser::new())),
52 Verbosity::Debug => topp2_ra_core(problem, (options, DebugVerboser::new())),
53 Verbosity::Trace => topp2_ra_core(problem, (options, TraceVerboser::new())),
54 }
55}
56
57fn topp2_ra_core(
59 problem: &Topp2Problem,
60 options_verboser: (&ReachSet2Options, impl Verboser),
61) -> Result<Vec<f64>, CoppError> {
62 let (options, mut verboser) = options_verboser;
63 if verboser.is_enabled(Verbosity::Summary) {
64 verboser.record_start_time();
65 crate::verbosity_log!(
66 Verbosity::Summary,
67 "\ntopp2_ra started: {} <= idx_s <= {}, a_start = {}, a_final = {}.",
68 problem.idx_s_interval.0,
69 problem.idx_s_interval.1,
70 problem.a_boundary.0,
71 problem.a_boundary.1,
72 );
73 }
74
75 let reach_set = reach_set2_backward(problem, options).map_err(|e| {
77 if verboser.is_enabled(Verbosity::Debug) {
78 crate::verbosity_log!(Verbosity::Debug, "{e:?}");
79 } else if verboser.is_enabled(Verbosity::Summary) {
80 crate::verbosity_log!(
81 Verbosity::Summary,
82 "topp2_ra: failed while computing backward reachable set."
83 );
84 }
85 e
86 })?;
87 let a_max = &reach_set.a_max;
88 let a_min = &reach_set.a_min;
89
90 if verboser.is_enabled(Verbosity::Debug) {
92 crate::verbosity_log!(Verbosity::Debug, "Forward pass started.");
93 }
94
95 let (idx_s_start, idx_s_final) = problem.idx_s_interval;
96 let n = idx_s_final - idx_s_start;
97 let mut a = vec![0.0; n + 1];
98 let mut a_prev = problem.a_boundary.0;
99 *a.first_mut().unwrap() = a_prev;
100
101 let mut a_b = Vec::<(f64, f64, f64)>::with_capacity(2 * problem.constraints.acc_rows());
102 for (k, (a_curr, &a_max_curr_, &a_min_curr_)) in
103 izip!(a.iter_mut(), a_max, a_min).enumerate().skip(1)
104 {
105 let idx_s = idx_s_start + k;
106 if verboser.is_enabled(Verbosity::Trace) {
107 crate::verbosity_log!(
108 Verbosity::Trace,
109 "\tForward pass at k = {k} (idx_s = {idx_s}): backward interval {a_min_curr_} <= a[k] <= {a_max_curr_}, a_prev = {a_prev}."
110 );
111 }
112
113 a_b.clear();
114 problem
115 .constraints
116 .fill_acc_topp2::<true>(&mut a_b, idx_s - 1);
117 let (mut a_max_curr, mut a_min_curr) = lp_1d::<true>(
119 a_b.iter().map(|&coeffs| {
120 (coeffs.0, coeffs.2 - coeffs.1 * a_prev)
123 }),
124 &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
125 );
126
127 if verboser.is_enabled(Verbosity::Trace) {
128 crate::verbosity_log!(
129 Verbosity::Trace,
130 "\t\tForward LP result before clipping: {a_min_curr} <= a[k] <= {a_max_curr}."
131 );
132 }
133
134 a_max_curr = a_max_curr.min(a_max_curr_);
135 a_min_curr = a_min_curr.max(a_min_curr_);
136 if verboser.is_enabled(Verbosity::Trace) {
137 crate::verbosity_log!(
138 Verbosity::Trace,
139 "\t\tAfter clipping with backward reachable set: {a_min_curr} <= a[k] <= {a_max_curr}."
140 );
141 }
142
143 if a_max_curr.is_nan()
144 || a_min_curr.is_nan()
145 || matches!(
146 approx_order(
147 a_max_curr,
148 a_min_curr,
149 options.a_cmp_abs_tol,
150 options.a_cmp_rel_tol,
151 ),
152 ApproxOrdering::Less
153 )
154 {
155 let err = CoppError::Infeasible(
156 "topp2_ra".into(),
157 format!(
158 "The reachable set is empty at index {} during the forward pass where a_max = {}, a_min = {}",
159 idx_s_start + k,
160 a_max_curr,
161 a_min_curr
162 ),
163 );
164 if verboser.is_enabled(Verbosity::Debug) {
165 crate::verbosity_log!(Verbosity::Debug, "{err:?}");
166 } else if verboser.is_enabled(Verbosity::Summary) {
167 crate::verbosity_log!(
168 Verbosity::Summary,
169 "topp2_ra: the forward pass failed at index {idx_s} due to infeasibility."
170 );
171 }
172 return Err(err);
173 }
174
175 if a_max_curr.is_infinite() {
176 let err = CoppError::Unbounded(
177 "topp2_ra".into(),
178 format!(
179 "The reachable set is unbounded at index {} during the forward pass where a_max = {}",
180 idx_s_start + k,
181 a_max_curr
182 ),
183 );
184 if verboser.is_enabled(Verbosity::Debug) {
185 crate::verbosity_log!(Verbosity::Debug, "{err:?}");
186 } else if verboser.is_enabled(Verbosity::Summary) {
187 crate::verbosity_log!(
188 Verbosity::Summary,
189 "topp2_ra: the forward pass failed at index {idx_s} due to unboundedness."
190 );
191 }
192 return Err(err);
193 }
194
195 if verboser.is_enabled(Verbosity::Debug)
196 && matches!(
197 approx_order(
198 a_max_curr,
199 a_min_curr,
200 options.a_cmp_abs_tol,
201 options.a_cmp_rel_tol,
202 ),
203 ApproxOrdering::Equal
204 )
205 {
206 crate::verbosity_log!(
207 Verbosity::Debug,
208 "The one-step forward reachable set at k = {k} (idx_s = {idx_s}) is degenerate since a_max and a_min are approximately equal at {}.",
209 0.5 * (a_max_curr + a_min_curr)
210 );
211 }
212
213 a_prev = a_max_curr;
214 *a_curr = a_prev;
215
216 if verboser.is_enabled(Verbosity::Trace) {
217 crate::verbosity_log!(
218 Verbosity::Trace,
219 "\t\tSelected maximal feasible state: a[k] = {a_prev}."
220 );
221 }
222 }
223
224 if verboser.is_enabled(Verbosity::Summary) {
225 crate::verbosity_log!(
226 Verbosity::Summary,
227 "topp2_ra: total elapsed time = {}.\n",
228 format_duration_human(verboser.elapsed())
229 );
230 }
231
232 Ok(a)
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::copp::InterpolationMode;
239 use crate::copp::copp2::stable::basic::{
240 Topp2ProblemBuilder, a_to_b_topp2, s_to_t_topp2, t_to_s_topp2,
241 };
242 use crate::copp::copp2::stable::reach_set2::ReachSet2OptionsBuilder;
243 use crate::path::{
244 Path, SplineConfig, add_symmetric_axial_limits_for_test, lissajous_path_for_test,
245 };
246 use crate::robot::robot_core::Robot;
247 use nalgebra::DMatrix;
248 use std::time::{Duration, Instant};
249
250 #[test]
251 fn test_topp2_ra() -> Result<(), CoppError> {
252 run_test_topp2_ra_repeated(1, false)
253 }
254
255 #[test]
256 #[ignore = "bindings"]
257 fn test_topp2_ra_bindings_parity() -> Result<(), CoppError> {
258 let dim = 3;
259 let num_waypoints = 8;
260 let n: usize = 201;
261 let pi = std::f64::consts::PI;
262
263 let waypoints = DMatrix::<f64>::from_fn(dim, num_waypoints, |axis, j| {
264 let s = j as f64 / (num_waypoints - 1) as f64;
265 match axis {
266 0 => 0.20 * (2.0 * pi * s).sin(),
267 1 => 0.15 * (1.5 * pi * s).cos(),
268 2 => 0.10 * s * (1.0 - s),
269 _ => unreachable!("dimension is fixed to 3"),
270 }
271 });
272 let path = Path::from_waypoints(&waypoints, SplineConfig::default())?;
273 let s = DMatrix::<f64>::from_fn(1, n, |_, j| j as f64 / (n - 1) as f64);
274
275 let mut robot = Robot::with_capacity(dim, n);
276 robot
277 .with_s(&s.as_view())?
278 .with_q_from_path_2nd(&path, 0, n)?
279 .with_q_from_path_3rd(&path, 0, n)?;
280 add_symmetric_axial_limits_for_test(&mut robot, 10.0, 50.0, Some(1000.0))?;
281
282 let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
283 let options = ReachSet2OptionsBuilder::new().build()?;
284 let a_profile = topp2_ra(&topp2_problem, &options)?;
285
286 let (t_final, t_s) = s_to_t_topp2(s.as_slice(), &a_profile, 0.0)?;
287 let s_t = t_to_s_topp2(
288 s.as_slice(),
289 &a_profile,
290 &t_s,
291 InterpolationMode::UniformTimeGrid(0.0, 1e-3, true),
292 )?;
293
294 crate::verbosity_log!(
295 Verbosity::Summary,
296 "TOPP2-RA Rust bindings parity test: t_final={:.17}, s_t.len={}",
297 t_final,
298 s_t.len()
299 );
300
301 Ok(())
302 }
303
304 #[test]
307 #[ignore = "slow"]
308 fn test_topp2_ra_robust() -> Result<(), CoppError> {
309 run_test_topp2_ra_repeated(10000, true)
310 }
311
312 fn run_test_topp2_ra_repeated(n_exp: usize, flag_print_step: bool) -> Result<(), CoppError> {
313 let mut tc_sum_ra = Duration::ZERO;
314 let mut tc_sum_interpolation = Duration::ZERO;
315 let mut t_final_sum = 0.0;
316
317 let options = ReachSet2OptionsBuilder::new()
318 .lp_feas_tol(1E-9)
319 .a_cmp_abs_tol(1E-9)
320 .a_cmp_rel_tol(1E-9)
321 .verbosity(Verbosity::Summary)
322 .build()?;
323
324 for i_exp in 0..n_exp {
325 let dim = 7;
326 let n: usize = 1000;
327 let mut robot = Robot::with_capacity(dim, n);
328
329 let mut rng = rand::rng();
330 let (s, path, _, _) = lissajous_path_for_test(dim, n, &mut rng).map_err(|e| {
331 CoppError::InvalidInput("lissajous_path_for_test".into(), e.to_string())
332 })?;
333 robot
334 .with_s(&s.as_view())?
335 .with_q_from_path_2nd(&path, 0, n)?;
336 add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, None)?;
337
338 let start = Instant::now();
339 let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
340 let a_profile = topp2_ra(&topp2_problem, &options)?;
341 let tc_topp2_ra = start.elapsed();
342
343 let b_profile = a_to_b_topp2(s.as_slice(), &a_profile)?;
344 assert!(
345 !izip!(
346 s.as_slice().windows(2),
347 a_profile.windows(2),
348 b_profile.iter()
349 )
350 .any(|(s_pair, a_pair, b)| {
351 let ds_double = 2.0 * (s_pair[1] - s_pair[0]);
352 let db = (a_pair[1] - a_pair[0]) / ds_double;
353 (*b - db).abs() > 1e-3
354 }),
355 "b_profile generation failed!"
356 );
357
358 let start = Instant::now();
359 let (t_final, t_s) = s_to_t_topp2(s.as_slice(), &a_profile, 0.0)?;
360 assert_eq!(t_s.len(), s.ncols());
361 let tc_interpolation = start.elapsed();
362 let s_t = t_to_s_topp2(
363 s.as_slice(),
364 &a_profile,
365 &t_s,
366 InterpolationMode::UniformTimeGrid(0.0, 1E-3, true),
367 )?;
368
369 tc_sum_ra += tc_topp2_ra;
370 tc_sum_interpolation += tc_interpolation;
371 t_final_sum += t_final;
372
373 if flag_print_step && ((i_exp + 1) % 100 == 0) {
374 crate::verbosity_log!(
375 Verbosity::Summary,
376 "Exp #{}: tc_topp2_ra = {:.4} ms, tc_interpolation = {:.4} ms, t_final = {:.4} s, s_t.len() = {}",
377 i_exp + 1,
378 tc_topp2_ra.as_secs_f64() * 1E3,
379 tc_interpolation.as_secs_f64() * 1E3,
380 t_final,
381 s_t.len()
382 );
383 }
384 }
385
386 crate::verbosity_log!(
387 Verbosity::Summary,
388 "Average over {} experiments: tc_topp2_ra = {:.6} ms, tc_interpolation = {:.6} ms, t_final = {:.6}",
389 n_exp,
390 tc_sum_ra.as_secs_f64() * 1E3 / n_exp as f64,
391 tc_sum_interpolation.as_secs_f64() * 1E3 / n_exp as f64,
392 t_final_sum / n_exp as f64
393 );
394
395 Ok(())
396 }
397}